package com.itlangzi.mybatis.interceptors;

import com.itlangzi.mybatis.dialect.Dialect;
import com.itlangzi.mybatis.dialect.DialectFactory;
import com.itlangzi.mybatis.factory.PaginationHelper;
import com.itlangzi.mybatis.page.Pagination;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.*;
import java.util.Map;
import java.util.Properties;

/**
 * @author hz-xiaolang
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PaginationInterceptor implements Interceptor {

    private static final String META_OBJECT_H = "h";

    private static final String META_OBJECT_TARGET = "target";

    private Logger logger = LoggerFactory.getLogger(getClass());


    @SuppressWarnings("rawtypes")
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();

        if (StatementHandler.class.isAssignableFrom(target.getClass())) {
            Connection connection = (Connection) invocation.getArgs()[0];
            DatabaseMetaData databaseMetaData = connection.getMetaData();
            String jdbcUrl = databaseMetaData.getURL();

            // 获取方言
            Dialect dialect = DialectFactory.buildDialectFromJDBCURL(jdbcUrl);
            logger.info("use dialect : {}", dialect);
            if (dialect == null) {
                logger.warn("dialect is not found");
                return invocation.proceed();
            }
            if (!dialect.supportsPage()) {
                logger.warn("Dialect {} now is Not Support page", dialect.getDialectName());
                return invocation.proceed();
            }
            StatementHandler statementHandler = (StatementHandler) target;
            BoundSql boundSql = statementHandler.getBoundSql();
            String originalSQL = boundSql.getSql();
            MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
            // 分离代理对象链(由于目标类可能被多个拦截器拦截，从而形成多次代理，通过下面的两次循环
            // 可以分离出最原始的的目标类)
            while (metaObject.hasGetter(META_OBJECT_H)) {
                metaObject = SystemMetaObject.forObject(metaObject.getValue(META_OBJECT_H));
            }
            // 分离最后一个代理对象的目标类
            while (metaObject.hasGetter(META_OBJECT_TARGET)) {
                metaObject = SystemMetaObject.forObject(metaObject.getValue(META_OBJECT_TARGET));
            }
            // 获取查询接口映射的相关信息
            MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
            // 不是查询语句
            if (!mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                logger.info("{} is Not Select; skiped!!! ", mappedStatement.getSqlCommandType());
                return invocation.proceed();
            }
            // 获取分页参数
            Object paramObj = boundSql.getParameterObject();
            Pagination<?> pagination = null;
            if (Pagination.class.isAssignableFrom(paramObj.getClass())) {
                pagination = (Pagination<?>) paramObj;
            } else if (Map.class.isAssignableFrom(paramObj.getClass())) {
                for (Object arg : ((Map) paramObj).values()) {
                    if (Pagination.class.isAssignableFrom(arg.getClass())) {
                        pagination = (Pagination<?>) arg;
                        break;
                    }
                }
            }
            if (pagination == null || pagination.getSize() <= 0) {
                return invocation.proceed();
            }

            String countSQL = dialect.buildCountSQL(originalSQL);

            logger.info("Count SQL => {}", countSQL);
            this.queryTotal(countSQL, mappedStatement, paramObj, boundSql, connection, pagination);

            String pageSQL = dialect.buildPageSQL(originalSQL, pagination.getPage(), pagination.getSize());

            logger.debug("Build Pagination SQL => {}", pageSQL);
            /**
             * <pre>
             *     禁用内存分页
             *     内存分页会查询所有结果出来处理（这个很吓人的），如果结果变化频繁这个数据还会不准.
             * </pre>
             */
            metaObject.setValue("delegate.boundSql.sql", pageSQL);
            metaObject.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
            metaObject.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);

            /**
             * <pre>
             *     保存分页信息, 可用于返回类型为Pagination的查询做后续处理
             *     注意：
             *     此处若返回类型不是Pagination，请勿设置该值，可能出现内存溢出
             *     若返回类型是Pagination则不会出现此问题
             *
             *     防止内存溢出：
             *     在拦截器后置处理中调用：PaginationHelper.clear()
             * </pre>
             */
            PaginationHelper.setPagination(pagination);

            return invocation.proceed();
        }
        return invocation.proceed();
    }

    private void queryTotal(String countSQL, MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql,
                            Connection connection, Pagination<?> pagination) {
        try (PreparedStatement preparedStatement = connection.prepareStatement(countSQL)) {
            DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject,
                    boundSql);
            parameterHandler.setParameters(preparedStatement);
            int totalElements = 0;
            try (ResultSet resultSet = preparedStatement.executeQuery()) {
                if (resultSet.next()) {
                    totalElements = resultSet.getInt(1);
                }
            }
            pagination.setTotalElements(totalElements);
        } catch (SQLException e) {
            logger.error("分页异常", e);
        }
    }

    @Override
    public Object plugin(Object target) {
        if (StatementHandler.class.isAssignableFrom(target.getClass())) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }

}
